Flutter 只是一个前端框架,无法提供一些系统能力,譬如蓝牙、相机、传感器等等,这就需要 Flutter 框架和原生的 Android/iOS 进行交互,来获取一些系统数据。为此,Flutter中提供了一个平台通道(platform channel),用于 Flutter 和原生平台的通信。平台通道正是 Flutter 和原生之间通信的桥梁,它也是 Flutter插件的底层基础设施。

接下来就以最新的方法,来一步步实现一个Flutter Plugin 插件,来达到和宿主系统的交互

之所以说最新的方法,是因为在我试验的过程中,搜了一些网上的例子(甚至是官方的文档),有一部分都还是旧的方法,一直出错,经过群友提醒,才找到正确的方法。

正是因为上面的这个原因,我还是先贴一下我的 Flutter 版本吧

Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel beta, v1.17.0, on Microsoft Windows [Version 10.0.18362.836], locale zh-CN)

创建 Flutter Plugin

Flutter Plugin 本身也是一个 Flutter Project,直接在 AndroidStudio 中就可以创建,只不过是在 Create New Flutter Project 处选择 Flutter Plugin 即可:

在红圈处可以看到 Select a "Plugin" when exposing an Android or iOS API for developers,就是我们要找的,然后项目名称和项目路径以及包名正常填写就可以,finish 完成创建。

先来看一下创建之后的目录结构:

主要注意以下几个部分:

  • android:安卓平台相关的代码
  • example:该插件使用的示例项目
  • ios:iOS 平台相关的代码
  • lib:里面的 dart 代码为插件代码

创建的 Plugin 可以直接运行,实际上运行的就是 example 里面的工程,它是一个调用原生系统 API,获取系统版本号的功能,其引用的就是我们创建的这个 Plugin,在 example 的 pubspec.yaml 中可以看到:

      flutterplugintest:
    # When depending on this package from a real application you should use:
    #   flutterplugintest: ^x.y.z
    # See https://dart.dev/tools/pub/dependencies#version-constraints
    # The example app is bundled with the plugin so we use a path dependency on
    # the parent directory to use the current plugin's version. 
    path: ../

Flutter 端

我们来看一下插件代码,也就是 lib 目录下的 flutterplugintest.dart

import 'dart:async';

import 'package:flutter/services.dart';

class Flutterplugintest {
  static const MethodChannel _channel =
      const MethodChannel('flutterplugintest');

  static Future<String> get platformVersion async {
    final String version = await _channel.invokeMethod('getPlatformVersion');
    return version;
  }
}

一共是两部分:

  1. 一个 MethodChannel 对象,其参数是一个字符串,用于指定该通道名称,需保证该通道名称的唯一性。

  2. 一个 platformVersion 方法,在方法中通过调用 MethodChannelinvokeMethod 方法来调用原生的代码来获取系统版本号 invokeMethod 方法参数是一个方法名。

    该方法是异步返回的,通过 Future 和 async 实现。

Android 端

Flutter 端的代码就是这么简单,再来看一下 Android 端的:

package com.example.flutterplugintest;

import androidx.annotation.NonNull;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;

/** FlutterplugintestPlugin */
public class FlutterplugintestPlugin implements FlutterPlugin, MethodCallHandler {
  private MethodChannel channel;

  @Override
  public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
    channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutterplugintest");
    channel.setMethodCallHandler(this);
  }

  public static void registerWith(Registrar registrar) {
    final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutterplugintest");
    channel.setMethodCallHandler(new FlutterplugintestPlugin());
  }

  @Override
  public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
    if (call.method.equals("getPlatformVersion")) {
      result.success("Android " + android.os.Build.VERSION.RELEASE);
    } else {
      result.notImplemented();
    }
  }

  @Override
  public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
    channel.setMethodCallHandler(null);
  }
}

直接查看 Android 端代码是飘红的,在 android 目录上右键 -- Flutter -- Open Android module in Android Studio 打开,就不飘红了。

代码是自动生成的,这里就需要说一下 Flutter 插件的一个坑了,Flutter 插件加载存在两个版本,一个是 FlutterPluginBinding (版本 >= 1.12)一个是 Registrar(版本 < 1.12),为了更好的兼容性,所以在 Android 端生成的代码中将两种方式都实现了一遍:

  • FlutterPluginBinding 版本:
    • onAttachedToEngine:插件被关联到 FlutterEngine 时调用,可以做一些初始化工作。可以看到在这个方法中设置了通道名称(该通道名称和 Flutter 端设置的相同),设置了方法回调。
    • onMethodCall:处理从 Flutter 接收的指定方法调用。在这个方法中继续一些宿主平台的操作。需要注意的是,该方法是执行在 UI 线程的。
    • onDetachedFromEngine:插件从 FlutterEngine 移除时调用,可以做一些清理工作。
  • Registrar 版本:

如果插件还涉及到 Activity 和 Service,还需要实现:

  • ActivityAware:用于 Activity 的生命周期管理和获取
  • ServiceAware:用于 Service 的生命周期管理和获取

简单示例

我们实现一个弹出 Toast 的功能:

Flutter 端

在 Flutter 端设置 通道名称以及设置需要调用的 android 端方法名:

android_taost_plugin.dart

import 'dart:async';

import 'package:flutter/services.dart';

class AndroidToastPlugin {
    ///定义 MethodChannel,名称唯一,需要和 android 端口相同
  static const MethodChannel _channel =
  const MethodChannel('ANDROID_TOAST_PLUGIN');

    ///提供调用方法
  static showToast() async {
      ///设置在 android 端调用的方法名
    await _channel.invokeMethod('showToast');
  }
}

Android 端

AndroidToastPlugin.java

package com.lixyz;

import android.app.Activity;
import android.content.Context;
import android.view.View;
import android.widget.Toast;

import androidx.annotation.NonNull;

import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.view.FlutterView;


/**
 * AndroidToastPlugin
 */
public class AndroidToastPlugin implements FlutterPlugin, MethodCallHandler {
    private MethodChannel channel;
    static Context context;

    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
        //1.12 之后创建 MethodChannel,并设置名称,需要和 Flutter 端相同
        channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "ANDROID_TOAST_PLUGIN");
        //设置方法调用回调
        channel.setMethodCallHandler(this);
        //获取 Context,以显示 Toast 时使用
        context = flutterPluginBinding.getApplicationContext();
    }


    /**
    *该方法用于兼容 1.12 之前的版本
    *和 onAttachedToEngine 方法执行相同的内容
    */
    public static void registerWith(Registrar registrar) {
        final MethodChannel channel = new MethodChannel(registrar.messenger(), "ANDROID_TOAST_PLUGIN");
        channel.setMethodCallHandler(new AndroidToastPlugin());
        context = registrar.context();
    }

    /**
    *1.12 之后,方法调用时候的回调
    *
    */    
    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
        //响应 Flutter 中调用的方法
        if (call.method.equals("showToast")) {
            Toast.makeText(context,"这是 android 端发出的 Toast",Toast.LENGTH_SHORT).show();
        } else {
            result.notImplemented();
        }
    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
        channel.setMethodCallHandler(null);
    }
}

如何使用 Flutter Plugin 呢?

通过执行以下命令,可以在命令行中对此包进行正确性验证:

flutter packages pub publish --dry-run

验证完成之后,就可以使用或者发布了。

项目中直接引用

看一下项目中的 example 中的 main.dart

import 'package:flutter/material.dart';
import 'package:lixyz/android_taost_plugin.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Plugin example app'),
        ),
        body: Center(
          child: MaterialButton(
            child: Text("点击弹出 Toast"),
            color: Colors.blue,
            onPressed: () {
              AndroidToastPlugin.showToast();
            },
          ),
        ),
      ),
    );
  }
}

调用插件的 showToast 方法,显示 Toast,就是这么简单。

发布到 Pub 仓库

---->[发布]----
flutter packages pub publish

---->[使用]----
dependencies:
  ia_path: ^0.0.1

然后所有人都可以使用你写的插件了。

发布到 Github

当公司内部写的插件,只想在公司内部共享不想公布到公网上,那么也可以将插件发布到 github 的私有仓库中,然后在项目中使用:

dependencies:
  flutter:
    sdk: flutter
  插件名(Flutter 插件项目中 yaml 中 name 属性指定的名称):
    git:
      url: github 地址,http:xxxxxxxx.git

需要注意的是,当使用 github 上公布的插件更新之后,使用该插件的工程想要获取最新版,需要先将 yaml 文件中的插件删除,然后 pub get,再添加,再 pub get

results matching ""

    No results matching ""